﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Web.UI.WebControls;
using log4net.Appender;
using log4net.Core;
using X.LOG.log4net.ElasticSearch.Models;
using X.LOG.log4net.CustomPatternLayout;
using Uri = System.Uri;

namespace X.LOG.log4net.ElasticSearch
{
    public class ElasticSearchAppender : BufferingAppenderSkeleton
    {
        private static readonly object LockObject = new object();

        static readonly string AppenderType = typeof(ElasticSearchAppender).Name;

        const int DefaultOnCloseTimeout = 30000;
        readonly ManualResetEvent workQueueEmptyEvent;

        IRepository repository;
        int queuedCallbackCount;
        List<FieldNameOverride> fieldNameOverrides = new List<FieldNameOverride>();
        List<FieldValueReplica> fieldValueReplicas = new List<FieldValueReplica>();

        private static ConcurrentQueue<logEvent> _BufferLogEvent = new ConcurrentQueue<logEvent>();
        //private static List<logEvent> _BufferLogEvent = new List<logEvent>();

        public ElasticSearchAppender()
        {
            workQueueEmptyEvent = new ManualResetEvent(true);
            OnCloseTimeout = DefaultOnCloseTimeout;
            base.ActivateOptions();
        }

        public string ConnectionString { get; set; }

        public int OnCloseTimeout { get; set; }

        public string RollingIndexNameDateFormat { get; set; } = "yyyy.MM.dd";

        public string IndexTypeName { get; set; } = "logEvent";

        public override void ActivateOptions()
        {
            ServicePointManager.Expect100Continue = false;

            try
            {
                Validate(ConnectionString);
            }
            catch (Exception ex)
            {
                HandleError("Failed to validate ConnectionString in ActivateOptions", ex);
                return;
            }

            // Artificially add the buffer size to the connection string so it can be parsed
            // later to decide if we should send a _bulk API call
            ConnectionString += string.Format(";BufferSize={0}", BufferSize);
            repository = CreateRepository(ConnectionString);
        }

        protected override void Append(LoggingEvent loggingEvent)
        {
            base.Append(loggingEvent);
        }

        protected override void Append(LoggingEvent[] loggingEvents)
        {
            base.Append(loggingEvents);
        }


        public void AddFieldNameOverride(FieldNameOverride fieldNameOverride)
        {
            fieldNameOverrides.Add(fieldNameOverride);
        }

        public void AddFieldValueReplica(FieldValueReplica fieldValueReplica)
        {
            fieldValueReplicas.Add(fieldValueReplica);
        }

        protected override void SendBuffer(LoggingEvent[] events)
        {
            var message = string.Empty;
            var strWriter = new StringWriter();
            foreach (var e in events)
            {
                //按ConversionPattern渲染strWriter
                RenderLoggingEvent(strWriter, e);
            }
            //message += "\r\n" + strWriter.ToString();
            BeginAsyncSend();
            if (TryAsyncSend(events)) return;
            EndAsyncSend();
            HandleError("Failed to async send logging events in SendBuffer");
        }

        protected override void OnClose()
        {
            if (_BufferLogEvent.Count > 0)
            {
                var bufferLogEvent = DeqeueLogEvent(_BufferLogEvent);
                repository.Add(bufferLogEvent, BufferSize);
                //_BufferLogEvent.Clear();
            }

            base.OnClose();

            if (TryWaitAsyncSendFinish()) return;
            HandleError("Failed to send all queued events in OnClose");
        }

        protected virtual IRepository CreateRepository(string connectionString)
        {
            log4net.ElasticSearch.Models.Uri.Init(RollingIndexNameDateFormat, IndexTypeName);

            var overrides = fieldNameOverrides.ToDictionary(x => x.Original, x => x.Replacement);
            var resolver = new CustomDataContractResolver
            {
                FieldNameChanges = overrides,
                FieldValueReplica = fieldValueReplicas,
            };
            return Repository.Create(connectionString, resolver);
        }

        protected virtual bool TryAsyncSend(IEnumerable<LoggingEvent> events)
        {
            return ThreadPool.QueueUserWorkItem(SendBufferCallback, logEvent.CreateMany(events));
        }

        protected virtual bool TryWaitAsyncSendFinish()
        {
            return workQueueEmptyEvent.WaitOne(OnCloseTimeout, false);
        }

        private void BeginAsyncSend()
        {
            workQueueEmptyEvent.Reset();
            Interlocked.Increment(ref queuedCallbackCount);
        }

        private void SendBufferCallback(object state)
        {
            try
            {
                var lstLogEvent = (IEnumerable<logEvent>)state;
                if (BufferSize <= 1)
                {
                    repository.Add(lstLogEvent, BufferSize);
                }
                else
                {
                    if (_BufferLogEvent.Count >= BufferSize)
                    {
                        lock (LockObject)
                        {
                            //缓存序列数量超出BufferSize则一次性提交
                            if (_BufferLogEvent.Count >= BufferSize)
                            {
                                //写入缓存序列
                                lstLogEvent.Do(e => _BufferLogEvent.Enqueue(e));
                                var bufferLogEvent = DeqeueLogEvent(_BufferLogEvent, BufferSize);
                                repository.Add(bufferLogEvent, BufferSize);
                            }
                            else
                            {
                                //写入缓存序列
                                lstLogEvent.Do(e => _BufferLogEvent.Enqueue(e));
                            }
                        }
                    }
                    else
                    {
                        //写入缓存序列
                        lstLogEvent.Do(e => _BufferLogEvent.Enqueue(e));
                    }
                }
            }
            catch (Exception ex)
            {
                HandleError("Failed to add logEvents to {0} in SendBufferCallback".With(repository.GetType().Name), ex);
            }
            finally
            {
                EndAsyncSend();
            }
        }

        private IEnumerable<logEvent> DeqeueLogEvent(ConcurrentQueue<logEvent> queue, int size = 0)
        {
            var list = new List<logEvent>();
            if (size == 0)
            {
                //取出所有
                logEvent e;
                while (queue.TryDequeue(out e))
                {
                    if (e != null)
                        list.Add(e);
                }
            }
            else
            {
                for (int i = 0; i < size; i++)
                {
                    logEvent e;
                    queue.TryDequeue(out e);
                    if (e != null)
                        list.Add(e);
                }
            }
            return list;
        }

        private void EndAsyncSend()
        {
            if (Interlocked.Decrement(ref queuedCallbackCount) > 0)
                return;
            workQueueEmptyEvent.Set();
        }

        void HandleError(string message)
        {
            ErrorHandler.Error("{0} [{1}]: {2}.".With(AppenderType, Name, message));
        }

        void HandleError(string message, Exception ex)
        {
            ErrorHandler.Error("{0} [{1}]: {2}.".With(AppenderType, Name, message), ex, ErrorCode.GenericFailure);
        }

        static void Validate(string connectionString)
        {
            if (connectionString == null)
            {
                throw new ArgumentNullException("connectionString");
            }

            if (connectionString.Length == 0)
            {
                throw new ArgumentException("connectionString is empty", "connectionString");
            }
        }
    }

    public class FieldNameOverride : IOptionHandler
    {
        public string Original { get; set; }

        public string Replacement { get; set; }

        public void ActivateOptions()
        {
        }
    }

    public class FieldValueReplica : IOptionHandler
    {
        public string Original { get; set; }

        public string Replica { get; set; }

        public void ActivateOptions()
        {
        }
    }
}